package org.chl.sqlsession.sqltemplate.core;

import org.chl.sqlsession.sqltemplate.script.*;
import org.chl.sqlsession.sqltemplate.token.GenericTokenParser;
import org.chl.sqlsession.sqltemplate.token.TokenHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.w3c.dom.*;
import org.xml.sax.*;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 * sql模板
 */
public class SqlTemplate {

	private static Logger logger = LoggerFactory.getLogger(SqlTemplate.class);

	private SqlFragment root;

	public SqlTemplate(SqlFragment root) {
		super();
		this.root = root;
	}

	public SqlMeta process(Object data) {

		Context context = new Context(data);

		calculate(context);

		parseParameter(context);

		return new SqlMeta(context.getSql(), context.getParameter());
	}

	private void parseParameter(final Context context) {
		String sql = context.getSql();
		GenericTokenParser parser = new GenericTokenParser("#{", "}",
				content -> {
                    Object value = OgnlCache.getValue(content,
                            context.getBinding());
                    if (value == null) {
                        throw new RuntimeException("Can not found "
                                + content + " value");
                    }
                    context.addParameter(value);
                    return "?";
                });
		sql = parser.parse(sql);
		context.setSql(sql);
	}

	private void calculate(Context context) {
		this.root.apply(context);
	}

	static public class SqlTemplateBuilder {

		private InputStream scriptFile;

		public SqlTemplateBuilder(InputStream scriptFile) {
			this.scriptFile = scriptFile;
		}

		public Map<String, SqlTemplate> build() {
			Map<String, SqlTemplate> sqlTemplateMap = new HashMap<>();
			try {
				Document document = buildXml(scriptFile);
				NodeList scripts = document.getElementsByTagName("script");
				XPathFactory pathFactory = XPathFactory.newInstance();
				XPath xpath = pathFactory.newXPath();
				for (int i = 0; i < scripts.getLength(); i++) {
					Element item = (Element)scripts.item(i);
					String namespace = item.getAttributes().getNamedItem("namespace").getTextContent();
					NodeList sqls = item.getElementsByTagName("sql");
					for (int j = 0; j < sqls.getLength(); j++) {
						Node sql = sqls.item(j);
						NodeList refs = (NodeList)xpath.evaluate(".//ref", sql, XPathConstants.NODESET);
						for (int x = 0; x < refs.getLength(); x++) {
							Node ref = refs.item(x);
							Node common = (Node)xpath.evaluate("//common[@id='" + ref.getAttributes().getNamedItem("id").getTextContent() + "']", item, XPathConstants.NODE);
							if (Objects.isNull(common)) {
								throw new RuntimeException(String.format("common id: %s not exist", ref.getAttributes().getNamedItem("id").getTextContent()));
							}
							NodeList childNodes = common.getChildNodes();
							for (int y = 0; y < childNodes.getLength(); y++) {
								ref.getParentNode().insertBefore(childNodes.item(y).cloneNode(true), ref);
							}
							ref.getParentNode().removeChild(ref);
						}
						String id = sql.getAttributes().getNamedItem("id").getTextContent();
						List<SqlFragment> contents = buildDynamicTag(sql);
						sqlTemplateMap.put(namespace + "." + id, new SqlTemplate(new MixedSqlFragment(contents)));
					}
				}
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
			return sqlTemplateMap;
		}

		private List<SqlFragment> buildDynamicTag(Node node) {
			List<SqlFragment> contents = new ArrayList<SqlFragment>();
			NodeList children = node.getChildNodes();
			for (int i = 0; i < children.getLength(); i++) {
				Node child = children.item(i);
				String nodeName = child.getNodeName();
				if (child.getNodeType() == Node.CDATA_SECTION_NODE
						|| child.getNodeType() == Node.TEXT_NODE) {
					String sql = child.getTextContent();
					TextFragment textFragment = new TextFragment(sql);
					contents.add(textFragment);
				} else if (child.getNodeType() == Node.ELEMENT_NODE) {
					TagHandler tagHandler = this.nodeHandlers.get(nodeName.toLowerCase());
					if(tagHandler != null ){
						tagHandler.handleNode(child, contents);
					}
				}
			}
			return contents;
		}

		private Document buildXml(InputStream scriptFile)
				throws ParserConfigurationException, SAXException, IOException {
			DocumentBuilderFactory factory = DocumentBuilderFactory
					.newInstance();
			factory.setValidating(true);

			factory.setNamespaceAware(false);
			factory.setIgnoringComments(true);
			factory.setIgnoringElementContentWhitespace(false);
			factory.setCoalescing(false);
			factory.setExpandEntityReferences(true);

			DocumentBuilder builder = factory.newDocumentBuilder();
			builder.setEntityResolver(new EntityResolver() {
				@Override
				public InputSource resolveEntity(String publicId,
						String systemId) throws SAXException, IOException {
					return new InputSource(new ClassPathResource("script-1.0.dtd").getInputStream());
				}
			});
			builder.setErrorHandler(new ErrorHandler() {
				@Override
				public void error(SAXParseException exception)
						throws SAXException {
					throw exception;
				}

				@Override
				public void fatalError(SAXParseException exception)
						throws SAXException {
					throw exception;
				}

				@Override
				public void warning(SAXParseException exception)
						throws SAXException {
				}
			});
			return builder.parse(scriptFile);
		}

		private Map<String, TagHandler> nodeHandlers = new HashMap<String, TagHandler>() {
			private static final long serialVersionUID = 7123056019193266281L;

			{
				put("trim", new TrimHandler());
				put("where", new WhereHandler());
				put("set", new SetHandler());
				put("foreach", new ForEachHandler());
				put("if", new IfHandler());
				put("choose", new ChooseHandler());
				put("when", new IfHandler());
				put("otherwise", new OtherwiseHandler());
			}
		};

		private interface TagHandler {

			/**
			 * 标签处理
			 * @param nodeToHandle
			 * @param targetContents
			 */
			void handleNode(Node nodeToHandle, List<SqlFragment> targetContents);
		}

		private class TrimHandler implements TagHandler {

			@Override
			public void handleNode(Node nodeToHandle,
					List<SqlFragment> targetContents) {
				List<SqlFragment> contents = buildDynamicTag(nodeToHandle);
				MixedSqlFragment mixedSqlFragment = new MixedSqlFragment(
						contents);
				
				NamedNodeMap attributes = nodeToHandle.getAttributes();
				Node prefixAtt = attributes
				.getNamedItem("prefix");
				
				String prefix = prefixAtt == null ? null :  prefixAtt.getTextContent();
				
				Node prefixOverridesAtt = attributes
				.getNamedItem("prefixOverrides");
				
				String prefixOverrides = prefixOverridesAtt.getTextContent();
				
				Node suffixAtt = attributes
				.getNamedItem("suffix");
				
				String suffix = suffixAtt == null ?  null :suffixAtt.getTextContent();
				
				Node suffixOverridesAtt = attributes
				.getNamedItem("suffixOverrides");
				
				String suffixOverrides = suffixOverridesAtt==null ? null :suffixOverridesAtt .getTextContent();
				TrimFragment trim = new TrimFragment(mixedSqlFragment, prefix,
						suffix, prefixOverrides, suffixOverrides);
				targetContents.add(trim);
			}
		}

		private class WhereHandler implements TagHandler {

			@Override
			public void handleNode(Node nodeToHandle,
					List<SqlFragment> targetContents) {
				List<SqlFragment> contents = buildDynamicTag(nodeToHandle);
				MixedSqlFragment mixedSqlFragment = new MixedSqlFragment(
						contents);
				WhereFragment where = new WhereFragment(mixedSqlFragment);
				targetContents.add(where);
			}
		}

		private class SetHandler implements TagHandler {

			@Override
			public void handleNode(Node nodeToHandle,
					List<SqlFragment> targetContents) {
				List<SqlFragment> contents = buildDynamicTag(nodeToHandle);
				MixedSqlFragment mixedSqlFragment = new MixedSqlFragment(
						contents);
				SetFragment set = new SetFragment(mixedSqlFragment);
				targetContents.add(set);
			}
		}

		private class ForEachHandler implements TagHandler {

			@Override
			public void handleNode(Node nodeToHandle,
					List<SqlFragment> targetContents) {
				List<SqlFragment> contents = buildDynamicTag(nodeToHandle);
				MixedSqlFragment mixedSqlFragment = new MixedSqlFragment(
						contents);
				NamedNodeMap attributes = nodeToHandle.getAttributes();
				Node collectionAtt= attributes.getNamedItem("collection");
				
				if(collectionAtt == null ){
					throw new RuntimeException(nodeToHandle.getNodeName() +" must has a collection attribute !" ) ;
				}
				
				String collection = collectionAtt.getTextContent();
				
				Node itemAtt = attributes.getNamedItem("item") ;
				
				String item = itemAtt == null ? "item" :itemAtt	.getTextContent();
				
				Node indexAtt = attributes.getNamedItem("index");
				
				String index = indexAtt == null ? "index" :indexAtt.getTextContent();
				
				Node openAtt = attributes.getNamedItem("open");
				
				String open = openAtt == null ? null : openAtt.getTextContent();
				
				Node closeAtt = attributes.getNamedItem("close");
				
				String close = closeAtt == null ? null : closeAtt.getTextContent();
				
				Node sparatorAtt = attributes.getNamedItem("separator");
				
				String separator = sparatorAtt == null  ? null :sparatorAtt.getTextContent();
				
				ForEachFragment forEachSqlFragment = new ForEachFragment(
						mixedSqlFragment, collection, index, item, open, close,
						separator);
				targetContents.add(forEachSqlFragment);
			}
		}

		private class IfHandler implements TagHandler {

			@Override
			public void handleNode(Node nodeToHandle,
					List<SqlFragment> targetContents) {
				List<SqlFragment> contents = buildDynamicTag(nodeToHandle);
				MixedSqlFragment mixedSqlFragment = new MixedSqlFragment(
						contents);
				
				NamedNodeMap attributes = nodeToHandle.getAttributes();
				
				Node testAtt = attributes.getNamedItem("test") ;
				
				if(testAtt == null ){
					throw new RuntimeException(nodeToHandle.getNodeName()+" must has test attribute ! ") ;
				}
				
				String test = testAtt.getTextContent();
				
				IfFragment ifSqlFragment = new IfFragment(mixedSqlFragment,
						test);
				targetContents.add(ifSqlFragment);
			}
		}

		private class OtherwiseHandler implements TagHandler {

			@Override
			public void handleNode(Node nodeToHandle,
					List<SqlFragment> targetContents) {
				List<SqlFragment> contents = buildDynamicTag(nodeToHandle);
				MixedSqlFragment mixedSqlFragment = new MixedSqlFragment(
						contents);
				targetContents.add(mixedSqlFragment);
			}
		}

		private class ChooseHandler implements TagHandler {

			@Override
			public void handleNode(Node nodeToHandle,
					List<SqlFragment> targetContents) {
				List<SqlFragment> whenSqlFragments = new ArrayList<SqlFragment>();
				List<SqlFragment> otherwiseSqlFragments = new ArrayList<SqlFragment>();
				handleWhenOtherwiseNodes(nodeToHandle, whenSqlFragments,
						otherwiseSqlFragments);
				SqlFragment defaultSqlFragment = getDefaultSqlFragment(otherwiseSqlFragments);
				ChooseFragment chooseSqlFragment = new ChooseFragment(
						whenSqlFragments, defaultSqlFragment);
				targetContents.add(chooseSqlFragment);
			}

			private void handleWhenOtherwiseNodes(Node chooseSqlFragment,
					List<SqlFragment> ifSqlFragments,
					List<SqlFragment> defaultSqlFragments) {
				NodeList children = chooseSqlFragment.getChildNodes();

				for (int i = 0; i < children.getLength(); i++) {
					Node child = children.item(i);
					if (child.getNodeType() == Node.ELEMENT_NODE) {
						String nodeName = child.getNodeName();
						TagHandler handler = nodeHandlers.get(nodeName);
						if (handler instanceof IfHandler) {
							handler.handleNode(child, ifSqlFragments);
						} else if (handler instanceof OtherwiseHandler) {
							handler.handleNode(child, defaultSqlFragments);
						}
					}
				}
			}

			private SqlFragment getDefaultSqlFragment(
					List<SqlFragment> defaultSqlFragments) {
				SqlFragment defaultSqlFragment = null;
				if (defaultSqlFragments.size() == 1) {
					defaultSqlFragment = defaultSqlFragments.get(0);
				} else if (defaultSqlFragments.size() > 1) {
					throw new RuntimeException(
							"Too many default (otherwise) elements in choose statement.");
				}
				return defaultSqlFragment;
			}
		}
	}
}
